#include "GlFrameBuffer.h"
#include <glad/gl.h>

namespace lazy {
    static const uint32_t s_MaxFrameBufferSize = 8192;
    namespace utils {

        static GLenum TextureTarget(bool multiSampled) {
            return multiSampled ? GL_TEXTURE_2D_MULTISAMPLE : GL_TEXTURE_2D;
        }

        static void CreateTextures(bool multiSampled, uint32_t *outID, uint32_t count) {
            glCreateTextures(TextureTarget(multiSampled), count, outID);
        }

        static void BindTexture(bool multiSampled, uint32_t id) {
            glBindTexture(TextureTarget(multiSampled), id);
        }

        static void AttachColorTexture(uint32_t id, int samples, GLenum internalFormat,  GLenum format, uint32_t width, uint32_t height, int index) {
            bool multiSampled = samples > 1;
            if (multiSampled) {
                glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, internalFormat, width, height, GL_FALSE);
            } else {
                glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, format, GL_UNSIGNED_BYTE, nullptr);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            }
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + index, TextureTarget(multiSampled), id, 0);
        }

        static void AttachDepthTexture(uint32_t id, int samples, GLenum format, GLenum attachmentType, uint32_t width, uint32_t height) {
            bool multiSampled = samples > 1;
            if (multiSampled) {
                glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, format, width, height, GL_FALSE);
            } else {
                glTexStorage2D(GL_TEXTURE_2D, 1, format, width, height);

                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            }
            glFramebufferTexture2D(GL_FRAMEBUFFER, attachmentType, TextureTarget(multiSampled), id, 0);
        }

        static bool IsDepthFormat(FrameBufferTextureFormat format) {
            switch (format) {
                case FrameBufferTextureFormat::DEPTH24STENCIL8:  return true;
            }
            return false;
        }

        static GLenum LazyFBTextureFormatToGL(FrameBufferTextureFormat format) {
            switch (format) {
                case FrameBufferTextureFormat::RGBA8: {
                    return GL_RGBA8;
                }
                case FrameBufferTextureFormat::RED_INTEGER: {
                    return GL_RED_INTEGER;
                }
            }
            return  0;
        }
    }


    GLFrameBuffer::GLFrameBuffer(const FramebufferSpecification &spec)
    : m_Specification(spec) {
        for (auto spec : m_Specification.Attachments.Attachments) {
            if (!utils::IsDepthFormat(spec.TextureFormat)) {
                m_ColorAttachmentSpecifications.emplace_back(spec);
            } else {
                m_DepthAttachmentSpecification = spec;
            }
        }
        Invalidate();
    }

    GLFrameBuffer::~GLFrameBuffer() {
        glDeleteFramebuffers(1, &m_BufferID);
        glDeleteTextures(m_ColorAttachments.size(), m_ColorAttachments.data());
        glDeleteTextures(1, &m_DepthAttachment);
    }

    void GLFrameBuffer::Invalidate() {
        if (m_BufferID) {
            glDeleteFramebuffers(1, &m_BufferID);
            glDeleteTextures(m_ColorAttachments.size(), m_ColorAttachments.data());
            glDeleteTextures(1, &m_DepthAttachment);
        }

        glCreateFramebuffers(1, &m_BufferID);
        glBindFramebuffer(GL_FRAMEBUFFER, m_BufferID);
        bool multiSample = m_Specification.Samples > 1;

        // Attachments
        if (m_ColorAttachmentSpecifications.size() > 0) {
            m_ColorAttachments.resize(m_ColorAttachmentSpecifications.size());
            utils::CreateTextures(multiSample, m_ColorAttachments.data(), m_ColorAttachments.size());
            for (size_t i = 0; i < m_ColorAttachments.size(); i++) {
                utils::BindTexture(multiSample, m_ColorAttachments[i]);
                switch (m_ColorAttachmentSpecifications[i].TextureFormat) {
                    case FrameBufferTextureFormat::RGBA8:
                        utils::AttachColorTexture(m_ColorAttachments[i], m_Specification.Samples, GL_RGBA8, GL_RGBA,  m_Specification.Width, m_Specification.Height, i);
                        break;
                    case FrameBufferTextureFormat::RED_INTEGER:
                        utils::AttachColorTexture(m_ColorAttachments[i], m_Specification.Samples, GL_R32I, GL_RED_INTEGER,  m_Specification.Width, m_Specification.Height, i);
                        break;
                }
            }
        }

        if (m_DepthAttachmentSpecification.TextureFormat != FrameBufferTextureFormat::None) {
            utils::CreateTextures(multiSample, &m_DepthAttachment, 1);
            utils::BindTexture(multiSample, m_DepthAttachment);
            switch (m_DepthAttachmentSpecification.TextureFormat) {
                case FrameBufferTextureFormat::DEPTH24STENCIL8:
                    utils::AttachDepthTexture(m_DepthAttachment, m_Specification.Samples, GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL_ATTACHMENT, m_Specification.Width, m_Specification.Height);
                    break;
            }
        }

        if (m_ColorAttachments.size() > 1) {
            GLenum buffers[4] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3 };
            glDrawBuffers(m_ColorAttachments.size(), buffers);
        } else if (m_ColorAttachments.empty()) {
            // Only depth-pass
            glDrawBuffer(GL_NONE);
        }

        glBindFramebuffer(GL_FRAMEBUFFER, 0);
    }

    void GLFrameBuffer::Bind() {
        glBindFramebuffer(GL_FRAMEBUFFER, m_BufferID);
        glViewport(0, 0, m_Specification.Width, m_Specification.Height);
    }

    void GLFrameBuffer::Unbind() {
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
    }

    void GLFrameBuffer::Resize(uint32_t width, uint32_t height) {
        if (width == 0 || height == 0 || width > s_MaxFrameBufferSize || height > s_MaxFrameBufferSize) {
            return;
        }
        m_Specification.Width = width;
        m_Specification.Height = height;
        Invalidate();
    }

    int GLFrameBuffer::ReadPixel(uint32_t attachmentIndex, int x, int y) {
        glReadBuffer(GL_COLOR_ATTACHMENT0 + attachmentIndex);
        int pixelData;
        glReadPixels(x, y, 1, 1, GL_RED_INTEGER, GL_INT, &pixelData);
        return pixelData;
    }

    void GLFrameBuffer::ClearAttachment(uint32_t attachmentIndex, int value) {
        auto& spec = m_ColorAttachmentSpecifications[attachmentIndex];
        glClearTexImage(m_ColorAttachments[attachmentIndex], 0,
                        utils::LazyFBTextureFormatToGL(spec.TextureFormat), GL_INT, &value);
    }

    uint32_t GLFrameBuffer::GetColorAttachmentBufferID(uint32_t index) const {
        return m_ColorAttachments[index];
    }

    const FramebufferSpecification &GLFrameBuffer::GetSpecification() const {
        return m_Specification;
    }

} // lazy